/*
 *
 * Copyright (C) 2012 Walter Lütgenau
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */
package offset.nodes.server.virtual.model;

import offset.nodes.server.virtual.model.sax.SAXContentSource;
import offset.nodes.server.virtual.model.sax.UpdatableSAXContentSource;
import offset.nodes.server.virtual.model.sax.RepositoryDataContentSource;
import offset.nodes.server.virtual.model.sax.ConcatenatorContentSource;
import offset.nodes.server.virtual.model.sax.NodeContentSource;
import offset.nodes.server.html.model.PageModel;
import java.io.ByteArrayOutputStream;
import java.util.HashMap;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Templates;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.URIResolver;
import javax.xml.transform.sax.SAXTransformerFactory;
import javax.xml.transform.sax.TransformerHandler;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import offset.nodes.Constants;
import offset.nodes.server.binary.controller.BinaryAttribute;
import offset.nodes.client.model.HttpUtils;
import offset.nodes.client.veditor.model.DataModel;
import offset.nodes.server.html.model.HttpContext;
import offset.nodes.server.model.Cache;
import offset.nodes.server.model.CacheObjectFactory;
import offset.nodes.server.virtual.model.sax.TransformerUtils;
import org.xml.sax.SAXException;

/**
 *
 * @author Walter Lütgenau
 */
public class VirtualModel extends PageModel {

    public static final String CACHE_DEFAULT_VIRTUAL_PAGE_INSTANCES = "defaultVirtualPageInstances";

    public VirtualModel(Session session) {
        super(session);
    }

    class MappingAttributes {

        private String export;
        private String root;
        private String template;

        public String getExport() {
            return export;
        }

        public void setExport(String export) {
            this.export = export;
        }

        public String getTemplate() {
            return template;
        }

        public void setTemplate(String template) {
            this.template = template;
        }

        public String getRoot() {
            return root;
        }

        public void setRoot(String root) {
            this.root = root;
        }
    }

    protected boolean isDataMapping(String html) {
        if (html.indexOf(DataModel.ATTRIBUTE_NAME_DATA) < 0)
            return false;
        return true;
    }

    protected MappingAttributes getMappingAttributes(String attributes) {
        MappingAttributes mapping = new MappingAttributes();
        StringTokenizer parser = new StringTokenizer(attributes, "\"");
        while (parser.hasMoreTokens()) {
            String key = parser.nextToken();
            if (key.endsWith("="))
                key = key.substring(0, key.length() - 1);
            String value = null;
            if (!parser.hasMoreTokens())
                break;
            value = parser.nextToken();
            if (key.endsWith(DataModel.ATTRIBUTE_NAME_TEMPLATE))
                mapping.setTemplate(value);
        }
        return mapping;
    }

    public UpdatableSAXContentSource createNodeContentSource(Node root) throws RepositoryException {
        return new NodeContentSource(getSession(), root);
    }

    public UpdatableSAXContentSource createRepositoryDataContentSource(String query, String root) {
        return new RepositoryDataContentSource(getSession(), query, root);
    }

    public SAXContentSource createHtmlDataContentSource(String query, String root) {
        RepositoryDataContentSource source = new RepositoryDataContentSource(getSession(), query, root);
        return source;
    }

    public SAXContentSource createRepositoryDataContentSource(UpdatableSAXContentSource[] sources, String[] prefixes) throws RepositoryException {
        return new ConcatenatorContentSource(sources, prefixes);
    }

    public String createMappedHtml(String styleSheetPath, String nodeTypeName, String dataPath, SAXContentSource data, HttpContext context) throws RepositoryException, TransformerConfigurationException, SAXException {
        Templates templates = StyleSheetCache.getInstance().getTemplates(this, styleSheetPath);
        if (templates == null)
            return "";
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        Result result = new StreamResult(out);
        TransformerFactory transformerFactory = TransformerFactory.newInstance();
        if (!transformerFactory.getFeature(SAXTransformerFactory.FEATURE))
            return "";
        SAXTransformerFactory saxFactory = (SAXTransformerFactory) transformerFactory;
        saxFactory.setURIResolver(new RepositoryResolver(this));
        TransformerHandler transformerHandler = saxFactory.newTransformerHandler(templates);
        transformerHandler.setResult(result);
        data.setContentHandler(transformerHandler);

        TransformerUtils.getInstance().setContext(context);
        data.writeContent();
        TransformerUtils.getInstance().setContext(null);
        
        return addTemplateInfo(new String(out.toByteArray()), nodeTypeName, styleSheetPath, dataPath);
    }

    protected String addTemplateInfo(String div, String nodeTypeName, String templatePath, String path) {
        StringBuffer buf = new StringBuffer();
        int endDiv = div.indexOf(">");
        buf.append(div.substring(0, endDiv));
        if (nodeTypeName != null)
            buf.append(" " + DataModel.ATTRIBUTE_NAME_SCHEMA + "=\"" + nodeTypeName + "\"");
        buf.append(" " + DataModel.ATTRIBUTE_NAME_TEMPLATE + "=\"" + templatePath + "\"");
        buf.append(" " + DataModel.ATTRIBUTE_NAME_PATH + "=\"" + path + "\"");
        buf.append(div.substring(endDiv));
        return buf.toString();
    }

    protected String updateDataMapping(String html, HttpContext context) throws RepositoryException, TransformerConfigurationException, SAXException {
        if (!isDataMapping(html))
            return html;
        MappingAttributes mapping = getMappingAttributes(html.substring(4, html.indexOf(">")));
        return createMappedHtml(mapping.getTemplate(), null, null, createRepositoryDataContentSource(mapping.getExport(), mapping.getRoot()), context);
    }

    protected String updateDataMappings(String html, HttpContext context) throws RepositoryException, TransformerConfigurationException, SAXException {
        String startTag = "<div";
        String endTag = "</div>";
        StringBuffer buf = new StringBuffer();
        int start = 0;
        int pos = 0;
        while ((start = html.indexOf(startTag, pos)) >= 0) {
            int end = html.indexOf(endTag, start) + endTag.length();
            String data = updateDataMapping(html.substring(start, end), context);
            buf.append(html.substring(pos, start));
            buf.append(data);
            pos = end;
        }
        buf.append(html.substring(pos));
        return buf.toString();
    }

    public static class RepositoryResolver implements URIResolver {

        VirtualModel model;

        public RepositoryResolver(VirtualModel model) {
            this.model = model;
        }

        public Source resolve(String href, String base) throws TransformerException {
            try {
                BinaryAttribute info = model.getFile(Constants.DIR_ETC + "/" + href);
                return new StreamSource(info.getInput());
            } catch (Exception e) {
                throw new TransformerException(e);
            }
        }
    }

    /**
     * Provide a factory to retrieve the default virtual page instances
     */
    class DefaultVirtualPageInstanceFactory implements CacheObjectFactory {

        protected NodeIterator getDefaultTemplateInstances() throws RepositoryException {
             String rootDir = Constants.DIR_NODES + Constants.PATH_SEPARATOR + Constants.CHILD_TEMPLATE_INSTANCES;
            String query = rootDir + "//element(*," + Constants.TYPE_VIRTUAL_PAGE_INSTANCE + ")";

            return executeQuery(query);
        }

        /**
         * Return the default virtual page instances as a hash map of primaryNodeType vs. UUID of the default
         * virtual page instance.
         *
         * @return the default virtual page instances.
         */
        public Object createCacheObject() {
            try {
                HashMap<String, String> cacheObject = new HashMap<String, String>();

                NodeIterator ni = getDefaultTemplateInstances();
                while (ni.hasNext()) {
                    Node virtualPageInstance = ni.nextNode();

                    String primaryNodeType = null;
                    String virtualPageInstanceUUID = null;
                    if (virtualPageInstance.hasProperty(Constants.JCR_UUID))
                        virtualPageInstanceUUID = virtualPageInstance.getUUID();
                    if (virtualPageInstance.hasProperty(Constants.PROP_NODETYPE))
                        primaryNodeType = virtualPageInstance.getProperty(Constants.PROP_NODETYPE).getString();

                    if (virtualPageInstanceUUID != null && primaryNodeType != null)
                        cacheObject.put(primaryNodeType, virtualPageInstanceUUID);
                }

                return cacheObject;
            } catch (RepositoryException ex) {
                Logger.getLogger(VirtualModel.class.getName()).log(Level.SEVERE, null, ex);
            }

            return null;
        }
    }

    /**
     * Return the cache of default virtual page instances, configured by the Constants.PROP_DEFAULT_TEMPLATE property
     * 
     * @param primaryNodeType
     * @return
     * @throws RepositoryException
     */
    public String getDefaultTemplateUUID(String primaryNodeType) throws RepositoryException {
        HashMap<String, String> defaultVirtualPageInstances = (HashMap<String, String>) Cache.getObject(CACHE_DEFAULT_VIRTUAL_PAGE_INSTANCES, new DefaultVirtualPageInstanceFactory());
        if (defaultVirtualPageInstances != null)
            return defaultVirtualPageInstances.get(primaryNodeType);

        return null;
    }

    /**
     * Is the node a multipage node?
     * 
     * @param node
     * @return
     * @throws RepositoryException
     */
    public boolean isMultipageNode(Node node) throws RepositoryException {
        return node.isNodeType(Constants.TYPE_ABSTRACT_MULTIPAGE) && node.hasProperty(Constants.PROP_EDITOR_VIRTUAL_PAGE_INSTANCE);
    }

    /**
     * Determine the URL to edit for a multipage node.
     * 
     * @param uuid
     * @return
     * @throws RepositoryException
     */
    public String getMultipageNodeEditUrl(Node node) throws RepositoryException {
        if (!isMultipageNode(node))
            return null;

        String editUrl = Constants.CONTEXTMAPPING_NODES + Constants.PATH_SEPARATOR + Constants.SERVLETMAPPING_REPOSITORY + Constants.QUERY_SEPARATOR;
        editUrl = HttpUtils.addParameter(editUrl, Constants.PAR_DATA_REFERENCE, node.getUUID());
        editUrl = HttpUtils.addParameter(editUrl, Constants.PAR_TEMPLATE_INSTANCE_REFERENCE, node.getProperty(Constants.PROP_EDITOR_VIRTUAL_PAGE_INSTANCE).getString());
        editUrl = HttpUtils.addParameter(editUrl, Constants.PAR_CONTENT_TYPE, Constants.CONTENT_TYPE_VIRTUAL);

        return editUrl;
    }


}
